using System;
using Org.BouncyCastle.Security;
using ch.ethz.ssh2.packets;
using System.Threading;
using ch.ethz.ssh2.crypto;
using ch.ethz.ssh2.signature;
using ch.ethz.ssh2.transport;
namespace ch.ethz.ssh2.auth
{
	
	/// <summary> AuthenticationManager.
	/// 
	/// </summary>
	/// <author>  Christian Plattner
	/// </author>
	/// <version>  2.50, 03/15/10
	/// </version>
	public class AuthenticationManager : MessageHandler
	{
		virtual internal byte[] NextMessage
		{
			get
			{
				while (true)
				{
					byte[] msg = deQueue();
					
					if (msg[0] != Packets.SSH_MSG_USERAUTH_BANNER)
						return msg;
					
					PacketUserauthBanner sb = new PacketUserauthBanner(msg, 0, msg.Length);
					
					banner = sb.Banner;
				}
			}
			
		}
		virtual public bool PartialSuccess
		{
			get
			{
				return isPartialSuccess;
			}
			
		}
		internal TransportManager tm;
		
		internal System.Collections.ArrayList packets = System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList(10));
		internal bool connectionClosed = false;
		
		internal System.String banner;
		
		internal System.String[] remainingMethods = new System.String[0];
		internal bool isPartialSuccess = false;
		
		internal bool authenticated = false;
		internal bool initDone = false;
		
		public AuthenticationManager(TransportManager tm)
		{
			this.tm = tm;
		}
		
		internal virtual bool methodPossible(System.String methName)
		{
			if (remainingMethods == null)
				return false;
			
			for (int i = 0; i < remainingMethods.Length; i++)
			{
				if (String.CompareOrdinal(remainingMethods[i], methName) == 0)
					return true;
			}
			return false;
		}
		
		internal virtual byte[] deQueue()
		{
			//lock (packets.SyncRoot)
            lock (packets)
			{
				while (packets.Count == 0)
				{
					if (connectionClosed)
						throw (System.IO.IOException) new System.IO.IOException("The connection is closed.",tm.ReasonClosedCause);
                    //System.Threading.Monitor.Enter(packets);
                    try
                    {
                        System.Threading.Monitor.Wait(packets);
                    }
                    catch (System.Threading.ThreadInterruptedException ign)
                    {
                        Thread.CurrentThread.Interrupt();
                    }
                    finally
                    {
                        //System.Threading.Monitor.Exit(packets);
                    }
				}
				/* This sequence works with J2ME */
				byte[] res = (byte[]) packets[0];
				packets.RemoveAt(0);
				return res;
			}
		}
		
		public virtual System.String[] getRemainingMethods(System.String user)
		{
			initialize(user);
			return remainingMethods;
		}
		
		private bool initialize(System.String user)
		{
			if (initDone == false)
			{
				tm.registerMessageHandler(this, 0, 255);
				
				PacketServiceRequest sr = new PacketServiceRequest("ssh-userauth");
				tm.sendMessage(sr.Payload);
				
				byte[] msg = NextMessage;
				new PacketServiceAccept(msg, 0, msg.Length);
				
				PacketUserauthRequestNone urn = new PacketUserauthRequestNone("ssh-connection", user);
				tm.sendMessage(urn.Payload);
				
				msg = NextMessage;
				
				initDone = true;
				
				if (msg[0] == Packets.SSH_MSG_USERAUTH_SUCCESS)
				{
					authenticated = true;
					tm.removeMessageHandler(this, 0, 255);
					return true;
				}
				
				if (msg[0] == Packets.SSH_MSG_USERAUTH_FAILURE)
				{
					PacketUserauthFailure puf = new PacketUserauthFailure(msg, 0, msg.Length);
					
					remainingMethods = puf.AuthThatCanContinue;
					isPartialSuccess = puf.PartialSuccess;
					return false;
				}
				
				throw new System.IO.IOException("Unexpected SSH message (type " + msg[0] + ")");
			}
			return authenticated;
		}
		
		public virtual bool authenticatePublicKey(System.String user, char[] PEMPrivateKey, System.String password, SecureRandom rnd)
		{
			try
			{
				initialize(user);
				
				if (methodPossible("publickey") == false)
					throw new System.IO.IOException("Authentication method publickey not supported by the server at this stage.");
				
				System.Object key = PEMDecoder.decode(PEMPrivateKey, password);
				
				if (key is DSAPrivateKey)
				{
					DSAPrivateKey pk = (DSAPrivateKey) key;
					
					byte[] pk_enc = DSASHA1Verify.encodeSSHDSAPublicKey(pk.PublicKey);
					
					TypesWriter tw = new TypesWriter();
					
					byte[] H = tm.SessionIdentifier;
					
					tw.writeString(H, 0, H.Length);
					tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST);
					tw.writeString(user);
					tw.writeString("ssh-connection");
					tw.writeString("publickey");
					tw.writeBoolean(true);
					tw.writeString("ssh-dss");
					tw.writeString(pk_enc, 0, pk_enc.Length);
					
					byte[] msg = tw.getBytes();
					
					DSASignature ds = DSASHA1Verify.generateSignature(msg, pk, rnd);
					
					byte[] ds_enc = DSASHA1Verify.encodeSSHDSASignature(ds);
					
					PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, "ssh-dss", pk_enc, ds_enc);
					tm.sendMessage(ua.Payload);
				}
				else if (key is RSAPrivateKey)
				{
					RSAPrivateKey pk = (RSAPrivateKey) key;
					
					byte[] pk_enc = RSASHA1Verify.encodeSSHRSAPublicKey(pk.PublicKey);
					
					TypesWriter tw = new TypesWriter();
					{
						byte[] H = tm.SessionIdentifier;
						
						tw.writeString(H, 0, H.Length);
						tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST);
						tw.writeString(user);
						tw.writeString("ssh-connection");
						tw.writeString("publickey");
						tw.writeBoolean(true);
						tw.writeString("ssh-rsa");
						tw.writeString(pk_enc, 0, pk_enc.Length);
					}
					
					byte[] msg = tw.getBytes();
					
					RSASignature ds = RSASHA1Verify.generateSignature(msg, pk);
					
					byte[] rsa_sig_enc = RSASHA1Verify.encodeSSHRSASignature(ds);
					
					PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, "ssh-rsa", pk_enc, rsa_sig_enc);
					tm.sendMessage(ua.Payload);
				}
				else
				{
					throw new System.IO.IOException("Unknown private key type returned by the PEM decoder.");
				}
				
				byte[] ar = NextMessage;
				
				if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS)
				{
					authenticated = true;
					tm.removeMessageHandler(this, 0, 255);
					return true;
				}
				
				if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE)
				{
					PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.Length);
					
					remainingMethods = puf.AuthThatCanContinue;
					isPartialSuccess = puf.PartialSuccess;
					
					return false;
				}
				
				throw new System.IO.IOException("Unexpected SSH message (type " + ar[0] + ")");
			}
			catch (System.IO.IOException e)
			{
				tm.close(e, false);
				throw (System.IO.IOException) new System.IO.IOException("Publickey authentication failed.",e);
			}
		}
		
		public virtual bool authenticateNone(System.String user)
		{
			try
			{
				initialize(user);
				return authenticated;
			}
			catch (System.IO.IOException e)
			{
				tm.close(e, false);
				throw (System.IO.IOException) new System.IO.IOException("None authentication failed.",e);
			}
		}
		
		public virtual bool authenticatePassword(System.String user, System.String pass)
		{
			try
			{
				initialize(user);
				
				if (methodPossible("password") == false)
					throw new System.IO.IOException("Authentication method password not supported by the server at this stage.");
				
				PacketUserauthRequestPassword ua = new PacketUserauthRequestPassword("ssh-connection", user, pass);
				tm.sendMessage(ua.Payload);
				
				byte[] ar = NextMessage;
				
				if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS)
				{
					authenticated = true;
					tm.removeMessageHandler(this, 0, 255);
					return true;
				}
				
				if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE)
				{
					PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.Length);
					
					remainingMethods = puf.AuthThatCanContinue;
					isPartialSuccess = puf.PartialSuccess;
					
					return false;
				}
				
				throw new System.IO.IOException("Unexpected SSH message (type " + ar[0] + ")");
			}
			catch (System.IO.IOException e)
			{
				tm.close(e, false);
				throw (System.IO.IOException) new System.IO.IOException("Password authentication failed.",e);
			}
		}
		
		public virtual bool authenticateInteractive(System.String user, System.String[] submethods, InteractiveCallback cb)
		{
			try
			{
				initialize(user);
				
				if (methodPossible("keyboard-interactive") == false)
					throw new System.IO.IOException("Authentication method keyboard-interactive not supported by the server at this stage.");
				
				if (submethods == null)
					submethods = new System.String[0];
				
				PacketUserauthRequestInteractive ua = new PacketUserauthRequestInteractive("ssh-connection", user, submethods);
				
				tm.sendMessage(ua.Payload);
				
				while (true)
				{
					byte[] ar = NextMessage;
					
					if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS)
					{
						authenticated = true;
						tm.removeMessageHandler(this, 0, 255);
						return true;
					}
					
					if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE)
					{
						PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.Length);
						
						remainingMethods = puf.AuthThatCanContinue;
						isPartialSuccess = puf.PartialSuccess;
						
						return false;
					}
					
					if (ar[0] == Packets.SSH_MSG_USERAUTH_INFO_REQUEST)
					{
						PacketUserauthInfoRequest pui = new PacketUserauthInfoRequest(ar, 0, ar.Length);
						
						System.String[] responses;
						
						try
						{
							responses = cb.replyToChallenge(pui.Name, pui.Instruction, pui.NumPrompts, pui.Prompt, pui.Echo);
						}
						catch (System.Exception e)
						{
							throw (System.IO.IOException) new System.IO.IOException("Exception in callback.",e);
						}
						
						if (responses == null)
							throw new System.IO.IOException("Your callback may not return NULL!");
						
						PacketUserauthInfoResponse puir = new PacketUserauthInfoResponse(responses);
						tm.sendMessage(puir.Payload);
						
						continue;
					}
					
					throw new System.IO.IOException("Unexpected SSH message (type " + ar[0] + ")");
				}
			}
			catch (System.IO.IOException e)
			{
				tm.close(e, false);
				throw (System.IO.IOException) new System.IO.IOException("Keyboard-interactive authentication failed.",e);
			}
		}
		
		public virtual void  handleMessage(byte[] msg, int msglen)
		{
			//lock (packets.SyncRoot)
            lock(packets)
            {
                //System.Threading.Monitor.Enter(packets);
                try
                {
                    if (msg == null)
                    {
                        connectionClosed = true;
                    }
                    else
                    {
                        byte[] tmp = new byte[msglen];
                        Array.Copy(msg, 0, tmp, 0, msglen);
                        packets.Add(tmp);
                    }

                    System.Threading.Monitor.PulseAll(packets);

                    if (packets.Count > 5)
                    {
                        connectionClosed = true;
                        throw new System.IO.IOException("Error, peer is flooding us with authentication packets.");
                    }
                }
                finally
                {
                    //System.Threading.Monitor.Exit(packets);
                }
			}
		}
	}
}